﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
namespace Dapper {
    /// <summary>
    /// Represents simple member map for one of target parameter or property or field to source DataReader column
    /// </summary>
    sealed partial class SimpleMemberMap :SqlMapper.IMemberMap {
        private readonly string _columnName;
        private readonly PropertyInfo _property;
        private readonly FieldInfo _field;
        private readonly ParameterInfo _parameter;
        /// <summary>
        /// Creates instance for simple property mapping
        /// </summary>
        /// <param name="columnName">DataReader column name</param>
        /// <param name="property">Target property</param>
        public SimpleMemberMap(string columnName ,PropertyInfo property) {
            if( columnName == null )
                throw new ArgumentNullException("columnName");
            if( property == null )
                throw new ArgumentNullException("property");
            _columnName = columnName;
            _property = property;
        }
        /// <summary>
        /// Creates instance for simple field mapping
        /// </summary>
        /// <param name="columnName">DataReader column name</param>
        /// <param name="field">Target property</param>
        public SimpleMemberMap(string columnName ,FieldInfo field) {
            if( columnName == null )
                throw new ArgumentNullException("columnName");
            if( field == null )
                throw new ArgumentNullException("field");
            _columnName = columnName;
            _field = field;
        }
        /// <summary>
        /// Creates instance for simple constructor parameter mapping
        /// </summary>
        /// <param name="columnName">DataReader column name</param>
        /// <param name="parameter">Target constructor parameter</param>
        public SimpleMemberMap(string columnName ,ParameterInfo parameter) {
            if( columnName == null )
                throw new ArgumentNullException("columnName");
            if( parameter == null )
                throw new ArgumentNullException("parameter");
            _columnName = columnName;
            _parameter = parameter;
        }
        /// <summary>
        /// DataReader column name
        /// </summary>
        public string ColumnName {
            get { return _columnName; }
        }
        /// <summary>
        /// Target member type
        /// </summary>
        public Type MemberType {
            get {
                if( _field != null )
                    return _field.FieldType;
                if( _property != null )
                    return _property.PropertyType;
                if( _parameter != null )
                    return _parameter.ParameterType;
                return null;
            }
        }
        /// <summary>
        /// Target property
        /// </summary>
        public PropertyInfo Property {
            get { return _property; }
        }
        /// <summary>
        /// Target field
        /// </summary>
        public FieldInfo Field {
            get { return _field; }
        }
        /// <summary>
        /// Target constructor parameter
        /// </summary>
        public ParameterInfo Parameter {
            get { return _parameter; }
        }
    }
    /// <summary>
    /// Represents default type mapping strategy used by Dapper
    /// </summary>
    sealed partial class DefaultTypeMap :SqlMapper.ITypeMap {
        private readonly List<FieldInfo> _fields;
        private readonly List<PropertyInfo> _properties;
        private readonly Type _type;
        /// <summary>
        /// Creates default type map
        /// </summary>
        /// <param name="type">Entity type</param>
        public DefaultTypeMap(Type type) {
            if( type == null )
                throw new ArgumentNullException("type");
            _fields = GetSettableFields(type);
            _properties = GetSettableProps(type);
            _type = type;
        }
#if DNXCORE50
        static bool IsParameterMatch(ParameterInfo[] x, ParameterInfo[] y)
        {
            if (ReferenceEquals(x, y)) return true;
            if (x == null || y == null) return false;
            if (x.Length != y.Length) return false;
            for (int i = 0; i < x.Length; i++)
                if (x[i].ParameterType != y[i].ParameterType) return false;
            return true;
        }
#endif
        internal static MethodInfo GetPropertySetter(PropertyInfo propertyInfo ,Type type) {
            if( propertyInfo.DeclaringType == type )
                return propertyInfo.GetSetMethod(true);
#if DNXCORE50
            return propertyInfo.DeclaringType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                    .Single(x => x.Name == propertyInfo.Name
                        && x.PropertyType == propertyInfo.PropertyType
                        && IsParameterMatch(x.GetIndexParameters(), propertyInfo.GetIndexParameters())
                        ).GetSetMethod(true);
#else
            return propertyInfo.DeclaringType.GetProperty(
                   propertyInfo.Name ,
                   BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ,
                   Type.DefaultBinder ,
                   propertyInfo.PropertyType ,
                   propertyInfo.GetIndexParameters().Select(p => p.ParameterType).ToArray() ,
                   null).GetSetMethod(true);
#endif
        }
        internal static List<PropertyInfo> GetSettableProps(Type t) {
            return t
                  .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                  .Where(p => GetPropertySetter(p ,t) != null)
                  .ToList();
        }
        internal static List<FieldInfo> GetSettableFields(Type t) {
            return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList();
        }
        /// <summary>
        /// Finds best constructor
        /// </summary>
        /// <param name="names">DataReader column names</param>
        /// <param name="types">DataReader column types</param>
        /// <returns>Matching constructor or default one</returns>
        public ConstructorInfo FindConstructor(string[] names ,Type[] types) {
            var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            foreach( ConstructorInfo ctor in constructors.OrderBy(c => c.IsPublic ? 0 : (c.IsPrivate ? 2 : 1)).ThenBy(c => c.GetParameters().Length) ) {
                ParameterInfo[] ctorParameters = ctor.GetParameters();
                if( ctorParameters.Length == 0 )
                    return ctor;
                if( ctorParameters.Length != types.Length )
                    continue;
                int i = 0;
                for( ;i < ctorParameters.Length;i++ ) {
                    if( !String.Equals(ctorParameters[i].Name ,names[i] ,StringComparison.OrdinalIgnoreCase) )
                        break;
                    if( types[i] == typeof(byte[]) && ctorParameters[i].ParameterType.FullName == SqlMapper.LinqBinary )
                        continue;
                    var unboxedType = Nullable.GetUnderlyingType(ctorParameters[i].ParameterType) ?? ctorParameters[i].ParameterType;
                    if( unboxedType != types[i]
                        && !(unboxedType.IsEnum() && Enum.GetUnderlyingType(unboxedType) == types[i])
                        && !(unboxedType == typeof(char) && types[i] == typeof(string))
                        && !(unboxedType.IsEnum() && types[i] == typeof(string)) ) {
                        break;
                    }
                }
                if( i == ctorParameters.Length )
                    return ctor;
            }
            return null;
        }
        /// <summary>
        /// Returns the constructor, if any, that has the ExplicitConstructorAttribute on it.
        /// </summary>
        public ConstructorInfo FindExplicitConstructor() {
            var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
#if DNXCORE50
            var withAttr = constructors.Where(c => c.CustomAttributes.Any(x => x.AttributeType == typeof(ExplicitConstructorAttribute))).ToList();
#else
            var withAttr = constructors.Where(c => c.GetCustomAttributes(typeof(ExplicitConstructorAttribute) ,true).Length > 0).ToList();
#endif
            if( withAttr.Count == 1 ) {
                return withAttr[0];
            }
            return null;
        }
        /// <summary>
        /// Gets mapping for constructor parameter
        /// </summary>
        /// <param name="constructor">Constructor to resolve</param>
        /// <param name="columnName">DataReader column name</param>
        /// <returns>Mapping implementation</returns>
        public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor ,string columnName) {
            var parameters = constructor.GetParameters();
            return new SimpleMemberMap(columnName ,parameters.FirstOrDefault(p => string.Equals(p.Name ,columnName ,StringComparison.OrdinalIgnoreCase)));
        }
        /// <summary>
        /// Gets member mapping for column
        /// </summary>
        /// <param name="columnName">DataReader column name</param>
        /// <returns>Mapping implementation</returns>
        public SqlMapper.IMemberMap GetMember(string columnName) {
            var property = _properties.FirstOrDefault(p => string.Equals(p.Name ,columnName ,StringComparison.Ordinal))
               ?? _properties.FirstOrDefault(p => string.Equals(p.Name ,columnName ,StringComparison.OrdinalIgnoreCase));
            if( property == null && MatchNamesWithUnderscores ) {
                property = _properties.FirstOrDefault(p => string.Equals(p.Name ,columnName.Replace("_" ,"") ,StringComparison.Ordinal))
                    ?? _properties.FirstOrDefault(p => string.Equals(p.Name ,columnName.Replace("_" ,"") ,StringComparison.OrdinalIgnoreCase));
            }
            if( property != null )
                return new SimpleMemberMap(columnName ,property);
            var field = _fields.FirstOrDefault(p => string.Equals(p.Name ,columnName ,StringComparison.Ordinal))
               ?? _fields.FirstOrDefault(p => string.Equals(p.Name ,columnName ,StringComparison.OrdinalIgnoreCase));
            if( field == null && MatchNamesWithUnderscores ) {
                field = _fields.FirstOrDefault(p => string.Equals(p.Name ,columnName.Replace("_" ,"") ,StringComparison.Ordinal))
                    ?? _fields.FirstOrDefault(p => string.Equals(p.Name ,columnName.Replace("_" ,"") ,StringComparison.OrdinalIgnoreCase));
            }
            if( field != null )
                return new SimpleMemberMap(columnName ,field);
            return null;
        }
        /// <summary>
        /// Should column names like User_Id be allowed to match properties/fields like UserId ?
        /// </summary>
        public static bool MatchNamesWithUnderscores { get; set; }
    }
    /// <summary>
    /// Implements custom property mapping by user provided criteria (usually presence of some custom attribute with column to member mapping)
    /// </summary>
    sealed partial class CustomPropertyTypeMap :SqlMapper.ITypeMap {
        private readonly Type _type;
        private readonly Func<Type ,string ,PropertyInfo> _propertySelector;
        /// <summary>
        /// Creates custom property mapping
        /// </summary>
        /// <param name="type">Target entity type</param>
        /// <param name="propertySelector">Property selector based on target type and DataReader column name</param>
        public CustomPropertyTypeMap(Type type ,Func<Type ,string ,PropertyInfo> propertySelector) {
            if( type == null )
                throw new ArgumentNullException("type");
            if( propertySelector == null )
                throw new ArgumentNullException("propertySelector");
            _type = type;
            _propertySelector = propertySelector;
        }
        /// <summary>
        /// Always returns default constructor
        /// </summary>
        /// <param name="names">DataReader column names</param>
        /// <param name="types">DataReader column types</param>
        /// <returns>Default constructor</returns>
        public ConstructorInfo FindConstructor(string[] names ,Type[] types) {
            return _type.GetConstructor(new Type[0]);
        }
        /// <summary>
        /// Always returns null
        /// </summary>
        /// <returns></returns>
        public ConstructorInfo FindExplicitConstructor() {
            return null;
        }
        /// <summary>
        /// Not implemented as far as default constructor used for all cases
        /// </summary>
        /// <param name="constructor"></param>
        /// <param name="columnName"></param>
        /// <returns></returns>
        public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor ,string columnName) {
            throw new NotSupportedException();
        }
        /// <summary>
        /// Returns property based on selector strategy
        /// </summary>
        /// <param name="columnName">DataReader column name</param>
        /// <returns>Poperty member map</returns>
        public SqlMapper.IMemberMap GetMember(string columnName) {
            var prop = _propertySelector(_type ,columnName);
            return prop != null ? new SimpleMemberMap(columnName ,prop) : null;
        }
    }
    internal class WrappedReader :IDataReader, IWrappedDataReader {
        private IDataReader reader;
        private IDbCommand cmd;
        public IDataReader Reader {
            get {
                var tmp = reader;
                if( tmp == null )
                    throw new ObjectDisposedException(GetType().Name);
                return tmp;
            }
        }
        IDbCommand IWrappedDataReader.Command {
            get {
                var tmp = cmd;
                if( tmp == null )
                    throw new ObjectDisposedException(GetType().Name);
                return tmp;
            }
        }
        public WrappedReader(IDbCommand cmd ,IDataReader reader) {
            this.cmd = cmd;
            this.reader = reader;
        }
        void IDataReader.Close() {
            if( reader != null )
                reader.Close();
        }
        int IDataReader.Depth {
            get { return Reader.Depth; }
        }
        DataTable IDataReader.GetSchemaTable() {
            return Reader.GetSchemaTable();
        }
        bool IDataReader.IsClosed {
            get { return reader == null ? true : reader.IsClosed; }
        }
        bool IDataReader.NextResult() {
            return Reader.NextResult();
        }
        bool IDataReader.Read() {
            return Reader.Read();
        }
        int IDataReader.RecordsAffected {
            get { return Reader.RecordsAffected; }
        }
        void IDisposable.Dispose() {
            if( reader != null )
                reader.Close();
            if( reader != null )
                reader.Dispose();
            reader = null;
            if( cmd != null )
                cmd.Dispose();
            cmd = null;
        }
        int IDataRecord.FieldCount {
            get { return Reader.FieldCount; }
        }
        bool IDataRecord.GetBoolean(int i) {
            return Reader.GetBoolean(i);
        }
        byte IDataRecord.GetByte(int i) {
            return Reader.GetByte(i);
        }
        long IDataRecord.GetBytes(int i ,long fieldOffset ,byte[] buffer ,int bufferoffset ,int length) {
            return Reader.GetBytes(i ,fieldOffset ,buffer ,bufferoffset ,length);
        }
        char IDataRecord.GetChar(int i) {
            return Reader.GetChar(i);
        }
        long IDataRecord.GetChars(int i ,long fieldoffset ,char[] buffer ,int bufferoffset ,int length) {
            return Reader.GetChars(i ,fieldoffset ,buffer ,bufferoffset ,length);
        }
        IDataReader IDataRecord.GetData(int i) {
            return Reader.GetData(i);
        }
        string IDataRecord.GetDataTypeName(int i) {
            return Reader.GetDataTypeName(i);
        }
        DateTime IDataRecord.GetDateTime(int i) {
            return Reader.GetDateTime(i);
        }
        decimal IDataRecord.GetDecimal(int i) {
            return Reader.GetDecimal(i);
        }
        double IDataRecord.GetDouble(int i) {
            return Reader.GetDouble(i);
        }
        Type IDataRecord.GetFieldType(int i) {
            return Reader.GetFieldType(i);
        }
        float IDataRecord.GetFloat(int i) {
            return Reader.GetFloat(i);
        }
        Guid IDataRecord.GetGuid(int i) {
            return Reader.GetGuid(i);
        }
        short IDataRecord.GetInt16(int i) {
            return Reader.GetInt16(i);
        }
        int IDataRecord.GetInt32(int i) {
            return Reader.GetInt32(i);
        }
        long IDataRecord.GetInt64(int i) {
            return Reader.GetInt64(i);
        }
        string IDataRecord.GetName(int i) {
            return Reader.GetName(i);
        }
        int IDataRecord.GetOrdinal(string name) {
            return Reader.GetOrdinal(name);
        }
        string IDataRecord.GetString(int i) {
            return Reader.GetString(i);
        }
        object IDataRecord.GetValue(int i) {
            return Reader.GetValue(i);
        }
        int IDataRecord.GetValues(object[] values) {
            return Reader.GetValues(values);
        }
        bool IDataRecord.IsDBNull(int i) {
            return Reader.IsDBNull(i);
        }
        object IDataRecord.this[string name] {
            get { return Reader[name]; }
        }
        object IDataRecord.this[int i] {
            get { return Reader[i]; }
        }
    }
    /// <summary>
    /// Describes a reader that controls the lifetime of both a command and a reader,
    /// exposing the downstream command/reader as properties.
    /// </summary>
    public interface IWrappedDataReader :IDataReader {
        /// <summary>
        /// Obtain the underlying reader
        /// </summary>
        IDataReader Reader { get; }
        /// <summary>
        /// Obtain the underlying command
        /// </summary>
        IDbCommand Command { get; }
    }
    /// <summary>
    /// Tell Dapper to use an explicit constructor, passing nulls or 0s for all parameters
    /// </summary>
    [AttributeUsage(AttributeTargets.Constructor ,AllowMultiple = false)]
    public sealed class ExplicitConstructorAttribute :Attribute {
    }
    // Define DAPPER_MAKE_PRIVATE if you reference Dapper by source
    // and you like to make the Dapper types private (in order to avoid
    // conflicts with other projects that also reference Dapper by source)
#if !DAPPER_MAKE_PRIVATE
    public partial class DynamicParameters {
    }
    public partial class DbString {
    }
    public partial class SimpleMemberMap {
    }
    public partial class DefaultTypeMap {
    }
    public partial class CustomPropertyTypeMap {
    }
    public partial class FeatureSupport {
    }
#endif
    internal static class TypeExtensions {
        public static bool IsValueType(this Type type) {
#if DNXCORE50
            return typeof(ValueType).IsAssignableFrom(type) && type != typeof(ValueType);
#else
            return type.IsValueType;
#endif
        }
        public static bool IsEnum(this Type type) {
#if DNXCORE50
            return typeof(Enum).IsAssignableFrom(type) && type != typeof(Enum);
#else
            return type.IsEnum;
#endif
        }
#if DNXCORE50
        public static TypeCode GetTypeCode(Type type)
        {
            if (type == null) return TypeCode.Empty;
            TypeCode result;
            if (typeCodeLookup.TryGetValue(type, out result)) return result;
            if (type.IsEnum())
            {
                type = Enum.GetUnderlyingType(type);
                if (typeCodeLookup.TryGetValue(type, out result)) return result;
            }
            return TypeCode.Object;
        }
        static readonly Dictionary<Type, TypeCode> typeCodeLookup = new Dictionary<Type, TypeCode>
        {
            {typeof(bool), TypeCode.Boolean },
            {typeof(byte), TypeCode.Byte },
            {typeof(char), TypeCode.Char},
            {typeof(DateTime), TypeCode.DateTime},
            {typeof(decimal), TypeCode.Decimal},
            {typeof(double), TypeCode.Double },
            {typeof(short), TypeCode.Int16 },
            {typeof(int), TypeCode.Int32 },
            {typeof(long), TypeCode.Int64 },
            {typeof(object), TypeCode.Object},
            {typeof(sbyte), TypeCode.SByte },
            {typeof(float), TypeCode.Single },
            {typeof(string), TypeCode.String },
            {typeof(ushort), TypeCode.UInt16 },
            {typeof(uint), TypeCode.UInt32 },
            {typeof(ulong), TypeCode.UInt64 },
        };
#else
        public static TypeCode GetTypeCode(Type type) {
            return Type.GetTypeCode(type);
        }
#endif
        public static MethodInfo GetPublicInstanceMethod(this Type type ,string name ,Type[] types) {
#if DNXCORE50
            var method = type.GetMethod(name, types);
            return (method != null && method.IsPublic && !method.IsStatic) ? method : null;
#else
            return type.GetMethod(name ,BindingFlags.Instance | BindingFlags.Public ,null ,types ,null);
#endif
        }
    }
}
